//----------------------------------------------------------------------------------------------------------------------
// Copyright (c) 2013 James Whitworth
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#if defined(_MSC_VER)
#define _CRT_SECURE_NO_WARNINGS
#endif
//----------------------------------------------------------------------------------------------------------------------
#include <sqtest/sqtScript.h>
//----------------------------------------------------------------------------------------------------------------------
#include <assert.h>
#include <stdio.h>
#include <string.h>
//----------------------------------------------------------------------------------------------------------------------
#include <gtest/gtest.h>
//----------------------------------------------------------------------------------------------------------------------

namespace sqt
{
//----------------------------------------------------------------------------------------------------------------------
// TestScript
//----------------------------------------------------------------------------------------------------------------------
Script::Script()
: m_contents(nullptr),
  m_contentsLength(0),
  m_lineOffsets(nullptr),
  m_lineCount(0),
  m_testNames(nullptr),
  m_testCount(0)
{
}

//----------------------------------------------------------------------------------------------------------------------
Script::Script(const Script &rhs)
: m_contents(nullptr),
  m_contentsLength(0),
  m_lineOffsets(nullptr),
  m_lineCount(0),
  m_testNames(nullptr),
  m_testCount(0)
{
  // copy the contents if any
  //
  if (rhs.m_contents != nullptr)
  {
    m_contents = new SQChar[rhs.m_contentsLength + 1];
    m_contentsLength = rhs.m_contentsLength;
    memcpy(m_contents, rhs.m_contents, sizeof(SQChar) * (rhs.m_contentsLength + 1));

    m_lineOffsets = new size_t[rhs.m_lineCount];
    m_lineCount = rhs.m_lineCount;
    memcpy(m_lineOffsets, rhs.m_lineOffsets, sizeof(size_t) * m_lineCount);
  }

  // copy the test info if any
  //
  if (rhs.m_testNames != nullptr)
  {
    m_testNames = new SQChar *[rhs.m_testCount];
    m_testCount = rhs.m_testCount;

    for (size_t i = 0; i != m_testCount; ++i)
    {
      auto testName = rhs.m_testNames[i];
      auto testNameLength = scstrlen(testName) + 1;
      m_testNames[i] = new SQChar[testNameLength];
      memcpy(m_testNames[i], testName, testNameLength);
    }
  }
}

//----------------------------------------------------------------------------------------------------------------------
Script::Script(Script &&rhs)
: m_contents(rhs.m_contents),
  m_contentsLength(rhs.m_contentsLength),
  m_lineOffsets(rhs.m_lineOffsets),
  m_lineCount(rhs.m_lineCount),
  m_testNames(rhs.m_testNames),
  m_testCount(rhs.m_testCount)
{
  rhs.m_contents = nullptr;
  rhs.m_contentsLength = 0;

  rhs.m_lineOffsets = nullptr;
  rhs.m_lineCount = 0;

  rhs.m_testNames = nullptr;
  rhs.m_testCount = 0;
}

//----------------------------------------------------------------------------------------------------------------------
Script::~Script()
{
  ClearContents();
}

//----------------------------------------------------------------------------------------------------------------------
Script &Script::operator = (const Script &rhs)
{
  // clear any existing data
  //
  ClearContents();

  // copy the contents if any
  //
  if (rhs.m_contents != nullptr)
  {
    m_contents = new SQChar[rhs.m_contentsLength + 1];
    m_contentsLength = rhs.m_contentsLength;
    memcpy(m_contents, rhs.m_contents, sizeof(SQChar) * (rhs.m_contentsLength + 1));

    m_lineOffsets = new size_t[rhs.m_lineCount];
    m_lineCount = rhs.m_lineCount;
    memcpy(m_lineOffsets, rhs.m_lineOffsets, sizeof(size_t) * m_lineCount);
  }

  // copy the test info if any
  //
  if (rhs.m_testNames != nullptr)
  {
    m_testNames = new SQChar *[rhs.m_testCount];
    m_testCount = rhs.m_testCount;

    for (size_t i = 0; i != m_testCount; ++i)
    {
      auto testName = rhs.m_testNames[i];
      auto testNameLength = scstrlen(testName) + 1;
      m_testNames[i] = new SQChar[testNameLength];
      memcpy(m_testNames[i], testName, testNameLength);
    }
  }

  return *this;
}

//----------------------------------------------------------------------------------------------------------------------
Script &Script::operator = (Script &&rhs)
{
  // clear any existing data
  //
  ClearContents();

  // move the data
  //
  m_contents = rhs.m_contents;
  m_contentsLength = rhs.m_contentsLength;

  m_lineOffsets = rhs.m_lineOffsets;
  m_lineCount = rhs.m_lineCount;

  m_testNames = rhs.m_testNames;
  m_testCount = rhs.m_testCount;

  // clear the rvalue data
  //
  rhs.m_contents = nullptr;
  rhs.m_contentsLength = 0;

  rhs.m_lineOffsets = nullptr;
  rhs.m_lineCount = 0;

  rhs.m_testNames = nullptr;
  rhs.m_testCount = 0;

  return *this;
}

//----------------------------------------------------------------------------------------------------------------------
void Script::ClearContents()
{
  // delete the contents if any
  //
  if (m_contents != nullptr)
  {
    delete [] m_contents;
    m_contents = nullptr;
    m_contentsLength = 0;

    delete [] m_lineOffsets;
    m_lineOffsets = nullptr;
    m_lineCount = 0;

    ClearTestInfo();
  }
}

//----------------------------------------------------------------------------------------------------------------------
const SQChar *Script::GetContents() const
{
  return m_contents;
}

//----------------------------------------------------------------------------------------------------------------------
const SQChar *Script::GetContents(size_t *contentsLength) const
{
  *contentsLength = m_contentsLength;
  return m_contents;
}

//----------------------------------------------------------------------------------------------------------------------
size_t Script::GetLineCount() const
{
  return m_lineCount;
}

//----------------------------------------------------------------------------------------------------------------------
const SQChar *Script::GetLine(size_t index) const
{
  const auto lineOffset = m_lineOffsets[index];
  return &m_contents[lineOffset];
}

//----------------------------------------------------------------------------------------------------------------------
size_t Script::GetLineLength(size_t index) const
{
  return m_lineOffsets[index + 1] - m_lineOffsets[index];
}

//----------------------------------------------------------------------------------------------------------------------
void Script::SetContents(const SQChar *contents)
{
  ClearContents();

  if (contents != nullptr)
  {
    m_contentsLength = scstrlen(contents);
    m_contents = new SQChar[m_contentsLength + 1];
    memcpy(m_contents, contents, sizeof(SQChar) * (m_contentsLength + 1));

    CalculateLineOffsets();
  }
}

//----------------------------------------------------------------------------------------------------------------------
void Script::SetContents(const SQChar *contents, size_t contentsLength)
{
  ClearContents();

  if (contents != nullptr)
  {
    m_contents = new SQChar[contentsLength + 1];
    m_contentsLength = contentsLength;
    memcpy(m_contents, contents, sizeof(SQChar) * (contentsLength + 1));

    // ensure the string is null terminated
    //
    m_contents[contentsLength] = '\0';

    CalculateLineOffsets();
  }
}

//----------------------------------------------------------------------------------------------------------------------
void Script::SetContentsTakeOwnership(SQChar *contents, size_t contentsLength)
{
  ClearContents();

  m_contents = contents;
  m_contentsLength = contentsLength;

  CalculateLineOffsets();
}

//----------------------------------------------------------------------------------------------------------------------
Script::ParseResult Script::ParseContents()
{
  if (m_contents == nullptr)
  {
    return kParseEmptyContents;
  }

  // remove any existing test info
  //
  ClearTestInfo();

  auto vm = sq_open(1024);

  sq_pushroottable(vm);

  // ensure that the test table exists
  //
  sq_pushstring(vm, _SC("test"), 4);
  sq_newtable(vm);
  sq_rawset(vm, -3);

  if (SQ_FAILED(sq_compilebuffer(vm, m_contents, m_contentsLength, _SC("Script::m_contents"), SQFalse)))
  {
    sq_close(vm);
    return kParseCompilationError;
  }

  sq_pushroottable(vm);

  if (SQ_FAILED(sq_call(vm, 1, SQFalse, SQFalse)))
  {
    sq_close(vm);
    return kParseExecutionError;
  }

  // pop the compiled buffer, root table will be on top
  //
  sq_poptop(vm);

  sq_pushstring(vm, _SC("test"), 4);
  sq_rawget(vm, -2);

  // push null to indicate to sq_next we are at the start of the iteration
  //
  sq_pushnull(vm);

  // count how many tests there are
  //
  while (SQ_SUCCEEDED(sq_next(vm, -2)))
  {
    auto keyType = sq_gettype(vm, -2);
    auto valueType = sq_gettype(vm, -1);

    if (keyType == OT_STRING && valueType == OT_CLOSURE)
    {
      ++m_testCount;
    }

    sq_pop(vm, 2);
  }

  if (m_testCount == 0)
  {
    sq_close(vm);
    return kParseNoTestsFound;
  }

  // remove the dead iterator
  //
  sq_poptop(vm);

  // now allocate the array for the test names
  //
  m_testNames = new SQChar *[m_testCount];

  // indicate to sq_next we want to iterate again
  //
  sq_pushnull(vm);

  // now allocate each test name
  //
  size_t index = 0;
  while (SQ_SUCCEEDED(sq_next(vm, -2)))
  {
    auto keyType = sq_gettype(vm, -2);
    auto valueType = sq_gettype(vm, -1);

    if (keyType == OT_STRING && valueType == OT_CLOSURE)
    {
      assert(index < m_testCount);

      const SQChar* testName = nullptr;
      sq_getstring(vm, -2, &testName);

      auto testNameLength = scstrlen(testName) + 1;
      m_testNames[index] = new SQChar[testNameLength];
      memcpy(m_testNames[index], testName, sizeof(SQChar) * testNameLength);

      ++index;
    }

    sq_pop(vm, 2);
  }

  sq_close(vm);

  return kParseSuccess;
}

//----------------------------------------------------------------------------------------------------------------------
void Script::ClearTestInfo()
{
  if (m_testNames != nullptr)
  {
    for (size_t i = 0; i != m_testCount; ++i)
    {
      auto testName = m_testNames[i];
      delete [] testName;
    }

    delete [] m_testNames;

    m_testNames = nullptr;
    m_testCount = 0;
  }
}

//----------------------------------------------------------------------------------------------------------------------
size_t Script::GetTestCount() const
{
  return m_testCount;
}

//----------------------------------------------------------------------------------------------------------------------
const SQChar *Script::GetTestName(size_t index) const
{
  return m_testNames[index];
}

//----------------------------------------------------------------------------------------------------------------------
void Script::CalculateLineOffsets()
{
  m_lineCount = 1;
  for (size_t i = 0; i != m_contentsLength; ++i)
  {
    if (m_contents[i] == '\n')
    {
      ++m_lineCount;
    }
  }

  m_lineOffsets = new size_t[m_lineCount];

  size_t currentLineIndex = 0;
  bool previousWasNewLine = true;
  for (size_t i = 0; i != m_contentsLength; ++i)
  {
    if (previousWasNewLine)
    {
      m_lineOffsets[currentLineIndex] = i;
      ++currentLineIndex;
    }

    previousWasNewLine = m_contents[i] == '\n';
  }
}

//----------------------------------------------------------------------------------------------------------------------
// LoadTestScript
//----------------------------------------------------------------------------------------------------------------------
std::shared_ptr<Script> LoadScript(const char *fileName)
{
  // open and work out the file size
  //
  auto file = fopen(fileName, "rb");
  if (file == nullptr)
  {
    return std::shared_ptr<Script>{};
  }

  auto result = fseek(file, 0, SEEK_END);
  if (result != 0)
  {
    fclose(file);
    return std::shared_ptr<Script>{};
  }

  const auto fileSize = ftell(file);

  // allocate and read the contents of the file in to m_contents
  //
  auto utf8Contents = new char[fileSize];

  result = fseek(file, 0, SEEK_SET);
  if (result != 0)
  {
    delete [] utf8Contents;
    fclose(file);
    return std::shared_ptr<Script>{};
  }

  fread(utf8Contents, sizeof(char), fileSize, file);
  fclose(file);

#if defined(SQUNICODE)
  std::mbstate_t state;
  const auto contentsLength = std::mbsrtowcs(nullptr, const_cast<const char **>(&utf8Contents), 0, &state);

  auto contents = new SQChar[contentsLength + 1];
  std::mbsrtowcs(contents, const_cast<const char **>(&utf8Contents), contentsLength + 1, &state);

  delete [] utf8Contents;
#else
  const auto contentsLength = fileSize;
  auto contents = utf8Contents;
#endif

  if (auto script = std::make_shared<Script>())
  {
    script->SetContentsTakeOwnership(contents, contentsLength);
    return script;
  }
  
  return std::shared_ptr<Script>{};
}

//----------------------------------------------------------------------------------------------------------------------
// LoadTestScriptAndParseContents
//----------------------------------------------------------------------------------------------------------------------
std::shared_ptr<Script> LoadScriptAndParseContents(const char *fileName)
{
  if (auto script = LoadScript(fileName))
  {
    script->ParseContents();
    return script;
  }

  return std::shared_ptr<Script>{};
}

} // namespace sqt
